Skip to content

Derive heading ids from source text, not smart-punctuation glyphs - #256

Merged
dereuromark merged 1 commit into
masterfrom
fix/heading-id-smart-punctuation
Jul 2, 2026
Merged

Derive heading ids from source text, not smart-punctuation glyphs#256
dereuromark merged 1 commit into
masterfrom
fix/heading-id-smart-punctuation

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

Problem

Heading auto-ids leaked smart-punctuation glyphs. The parser bakes smart punctuation into heading text (apostrophe as U+2019, --/--- as en/em dash, ... as ellipsis). Those glyphs are non-ASCII, so the slug step preserved them: # Bob's Guide produced id="Bob<U+2019>s-Guide" with a real curly apostrophe.

The djot.js reference does not do this. Its smart_punctuation nodes carry the ASCII source (', --, ...) and it slugs that source, so ids never contain typographic glyphs.

Fix

Reverse the core smart-punctuation glyphs back to their ASCII source in HeadingIdTracker before slugging, so ids are derived from source text and align with the reference.

Heading Before After
# Bob's Guide Bob's-Guide (curly) Bob-s-Guide
# a -- b a-<endash>-b a-b
# Say "Hello" Say-"Hello" (curly) Say-Hello

Behavior change

Every heading containing an apostrophe, quote, or dash now gets a different id, so links to the old glyph-bearing anchors need updating. This is intentional and matches the djot.js reference plus the agreed direction in jgm/djot#391 and jgm/djot#393 (strike punctuation exceptions; ids derived from source text).

Known limitation

Smart punctuation is baked into plain text nodes, so a literal author-typed typographic glyph (a real U+2013 in the source) is indistinguishable from a parser-generated one and is reversed the same way. Rare, and documented in the code and reference docs. A full fix needs node-level source tracking (follow-up).

Docs updated in docs/reference/enhancements.md (normalization rules + examples).

The parser bakes smart punctuation into heading text (apostrophe as
U+2019, `--`/`---` as en/em dash, `...` as ellipsis). Those glyphs are
non-ASCII, so the slug step preserved them and they leaked into the id
(e.g. id="Bob<U+2019>s-Guide").

Reverse the core smart-punctuation glyphs back to their ASCII source
before slugging, so ids are derived from the source text and match the
djot.js reference. Example: "# Bob's Guide" now yields id="Bob-s-Guide".

Note: this changes ids for every heading containing an apostrophe,
quote, or dash; links to the old glyph-bearing anchors need updating.
@dereuromark dereuromark added the bug Something isn't working label Jul 2, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.36%. Comparing base (f584070) to head (7c5e88c).

Additional details and impacted files
@@            Coverage Diff            @@
##             master     #256   +/-   ##
=========================================
  Coverage     92.35%   92.36%           
- Complexity     3630     3631    +1     
=========================================
  Files           109      109           
  Lines         10253    10263   +10     
=========================================
+ Hits           9469     9479   +10     
  Misses          784      784           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dereuromark
dereuromark merged commit 30c1c87 into master Jul 2, 2026
6 checks passed
@dereuromark
dereuromark deleted the fix/heading-id-smart-punctuation branch July 2, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant